ECMAScript 双月报告:Array.fromAsync 进入 Stage 3
作者:穹心
审校:昭朗
在本次 TC39 会议中,或许是由于在亚洲时区(东京时间)举办的原因,整体提交的提案数量较少,也仅有三个提案取得了阶段性进展。另外,本次会议中没有提案进入到 Stage 4 阶段。
Stage 2 → Stage 3
提案从 Stage 2 进入到 Stage 3 有以下几个门槛:
撰写了包含提案所有内容的标准文本,并有指定的 TC39 成员审阅并签署了同意意见; ECMAScript 编辑签署了同意意见。
Array.fromAsync
提案链接:proposal-array-from-async[1]
在 JavaScript 中,Array.from
方法用于从一个类数组或可迭代对象(Iterable,即部署了 [Symbol.iterator]
接口的对象)创建一个新的数组。
在从可迭代对象创建数组时,其实际上等价于以下的代码:
const arr = [];
for (const v of iterable) {
arr.push(v);
}
// 等价于
const arr = Array.from(iterable);
然而还有一种常见的场景是,从异步迭代对象(Async Iteratable,即部署了 [Symbol.asyncIterator]
接口的对象)类型创建数组,此时常见的方式是使用 for await of
语法:
const arr = [];
for await (const v of asyncIterable) {
arr.push(v);
}
而为了在语言层面支持这一能力,此提案引入了 Array.fromAsync
方法,来从异步迭代对象生成数组。
此方法会从迭代对象(包括 Iteratable 与 Async Iteratable)立即生成一个 Promise ,其成功 resolve 将返回一个数组:
function sleep() {
return new Promise((res, rej) => {
setTimeout(res, 1000);
});
}
Array.fromAsync = async (source) => {
const arr = [];
for await (const entry of source) {
arr.push(entry);
}
return arr;
};
const arr = [1, 2, 3, 4];
// 异步迭代
async function* asyncGen() {
for (const i of arr) {
await sleep();
yield i;
}
}
// 同步迭代
function* syncGen() {
for (const i of arr) {
yield i;
}
}
(async () => {
console.log(Array.fromAsync(syncGen()));
console.log(Array.fromAsync(asyncGen()));
})();
以上调用均会立刻输出两个 Promise :
Promise { <pending> }
Promise { <pending> }
而如果我们 await 这两个 Promise,那么对 asyncGen()
的调用将会创建一个异步迭代器(Async Iterator),然后依次等待每一个内部的 Promise resolve,再将其值添加进结果数组,最后返回这个数组:
// 来自于 syncGen() 的调用会立刻返回
[ 1, 2, 3, 4 ]
// 来自于 asyncGen() 的调用等待 4s 后才打印
[ 1, 2, 3, 4 ]
而如果同步可迭代对象也返回了 Promise ,那么 fromAsync 同样会顺序地依次等待每一个 Promise resolve:
// 异步迭代
async function* asyncGen() {
for (const i of arr) {
await sleep();
yield i;
}
}
// 生成 Promise 的同步迭代
function* syncGenWithPromise() {
for (const i of arr) {
yield sleep().then(() => i);
}
}
(async () => {
console.log(await Array.fromAsync(syncGenWithPromise()));
console.log(await Array.fromAsync(asyncGen()));
})();
// 等待 4s 后打印
[ 1, 2, 3, 4 ]
// 再等待 4s 后打印
[ 1, 2, 3, 4 ]
但如果使用 Array.from
方法来迭代返回 Promise 的同步可迭代对象,实际上其中的各个 Promise 会是彼此独立的,即无需等待上一个 Promise settle :
// 生成 Promise 的同步迭代
function* syncGenWithPromise() {
for (const i of arr) {
// 越往后,越快 resolve
yield sleep(2000 - i * 100).then(() => {
console.log(`${i} resolved`);
return i;
});
}
}
(async () => {
console.log(await Promise.all(Array.from(syncGenWithPromise())));
})();
4 resolved
3 resolved
2 resolved
1 resolved
[ 1, 2, 3, 4 ]
最后,你可能会想到与 Array.fromAsync
有些相似的 Promise.all
方法,但Promise.all
将并行地等待内部所有的 Promise resolve,然后一次性返回所有结果:
// 生成 Promise 的同步迭代
function* syncGenWithPromise() {
for (const i of arr) {
yield sleep().then(() => i);
}
}
(async () => {
console.log(await Promise.all(Array.from(syncGenWithPromise())));
})();
// 只需等待 1s
[ 1, 2, 3, 4 ]
Stage 1 → Stage 2
从 Stage 1 进入到 Stage 2 需要完成撰写包含提案所有内容的标准文本的初稿。
Well-formed Unicode strings
提案链接:proposal-is-usv-string[2]
ECMAScript 字符串都是 UTF-16 编码的字符串。在 Web API 中,我们可以发现有些 API (如 URL、URLSearchParams 等等系列 API)都声明了需要 USVString 作为参数。什么是 USVString?USV 代表 Unicode Scalar Value,即 Unicode 标量值。根据 Unicode 定义,Unicode 的码位(Code Point)可以分成几个类别,分别是图形码(Graphic),格式码(Format),控制码(Control),私有码(Private-Use),代理码(Surrogate),非字符码(Noncharacter),与保留码(Reserved)。而其中的代理码又分成了高位代理码与低位代码码,只有当一个高位代码码与一个低位代理码组合成一个代理码对,才是一个合法的 Unicode 字符。
目前,JavaScript 字符串并不限制这个字符串的值是否是合法的 Unicode 值,比如我们可以编码一个字符串只有高位代理码,而没有低位代理码等等。而如严格的 Web URL API 定义必须要求参数字符串是合法的 Unicode 标量值,因此我们需要有方法能够去区分一个字符串是否是合法的 Unicode 标量值。
这个提案提出为 ECMAScript 引入新的内置方法 String.prototype.isWellFormed
, 用于检查这个字符串是否是一个合法的 Unicode 标量值:
'\ud800'.isWellFormed(); // => false
'\ud800\udc00'.isWellFormed(); // => true
另外此提案也提供了 String.prototype.toWellFormed
方法,来将普通字符串转换到一个格式正确的 USV 字符串。类似的,NodeJs 中也提供了 util.toUSVString
这样的方法来实现此功能。
Stage 0 → Stage 1
从 Stage 0 进入到 Stage 1 有以下门槛:
找到一个 TC39 成员作为 champion 负责这个提案的演进; 明确提案需要解决的问题与需求和大致的解决方案; 有问题、解决方案的例子; 对 API 形式、关键算法、语义、实现风险等有讨论、分析。Stage 1 的提案会有可预见的比较大的改动,以下列出的例子并不代表提案最终会是例子中的语法、语义。
Extractor Objects
提案链接:proposal-extractors[3]
提取器语法是 Scala 中用于快速提取实例属性的语法糖,在 Scala 中,我们可以通过 apply 方法定义类的实例化方法,通过 unapply 方法(即提取器)反转这个过程——从实例获得实例化时的入参。
如以下的 Scala 代码:
object UserId:
// 生成一个 UserId 字符串
def apply(name: String) = s"userId--$name"
// 从 UserId 字符串获得生成时的 name
def unapply(userId: String): Option[String] =
val stringArray: Array[String] = userId.split("--")
if stringArray.tail.nonEmpty then Some(stringArray.tail) else None
// 定义了 apply 方法后,才能通过这种方式进行实例化
val userId1 = UserId("小明") // userId-小明
// 通过提取器获得其 name
val UserId(name1) = userId1
println(name1) // 小明
// 也可以直接应用于字符串,在无法提取时会返回一个 None 类型
val UserId(name2) = "userId-大明"
println(name2) // 大明
而其提案即旨在为 ECMAScript 引入提取器语法,包括数组提取器与对象提取器两种使用形式,如以下 JavaScript 代码:
class Foo {
constructor(foo, bar, baz) {
this.foo = foo;
this.bar = bar;
this.baz = baz;
}
}
const foo = new Foo();
// 提取 foo bar
const Foo(arg1, arg2) = foo;
// 提取 foo baz
const Foo{foo, baz} = foo;
以上代码使用的是绑定模式语法(Binding Pattern),你也可以使用分配模式(Assignment Pattern),有点类似函数声明与函数表达式的区别:
Foo(arg1, arg2) = foo;
Foo{foo, baz} = foo;
而提取器语法也可以和 Pattern Matching[4] 提案协作,我们还是先看看 Scala 中这两种语法的组合:
userId1 match
case UserId(name1) => println(name1) // 小明
case _ => println("提取用户 ID 失败")
而在 ECMAScript 中,结合提取器语法和模式匹配,我们能够实现在解构赋值的同时进行校验或是二次处理,如以下的例子:
// 确保值为 Instance 类型,即一个不包含时区信息的精确时间
const InstantExtractor = {
// 通过部署 Symbol.matcher 接口实现自定义匹配
[Symbol.matcher]: value =>
value instanceof Temporal.Instant ? { matched: true, value: [value] } :
value instanceof Date ? { matched: true, value: [Temporal.Instant.fromEpochMilliseconds(value.getTime())] } :
typeof value === "string" ? { matched: true, value: [Temporal.Instant.from(value)] } :
{ matched: false };
}
};
class Book {
constructor({
title,
// 在解构出这个值的同时,对其进行格式转换
createdAt: InstantExtractor(createdAt) = Temporal.Now.instant(),
modifiedAt: InstantExtractor(modifiedAt) = createdAt
}) {
this.title = title;
this.createdAt = createdAt;
this.modifiedAt = modifiedAt;
}
}
而这也是解构赋值自 ES6 加入 JavaScript 以来一个呼声强烈的功能——解构时的额外处理逻辑。通过解构赋值结合提取器,我们能够将值的读取、校验与处理合并在一处,确保在后续消费时可以直接使用。
总结
由贺师俊牵头,阿里巴巴前端标准化小组等多方参与组建的 JavaScript 中文兴趣小组(JSCIG,JavaScript Chinese Interest Group)在 GitHub 上开放讨论各种 ECMAScript 的问题,非常欢迎有兴趣的同学参与讨论:esdiscuss。
参考资料
proposal-array-from-async: https://github.com/tc39/proposal-array-from-async
[2]proposal-is-usv-string: https://github.com/tc39/proposal-is-usv-string
[3]proposal-extractors: https://github.com/tc39/proposal-extractors
[4]Pattern Matching: https://github.com/tc39/proposal-pattern-matching